<?php

/*
 * Copyright (C) 2009 - 2011 Pham Cong Dinh
 *
 * This file is part of Spica.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

// namespace spica\core\session\store;

/**
 * This class provides Redis storage backend for session data.
 *
 * @category   spica
 * @package    core
 * @subpackage session\store
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      April 17, 2010
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: Redis.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaSessionRedisStore /* implements SpicaSessionStore */
{
    /**
     * Session lifetime.
     *
     * @var int
     */
    protected $_lifeTime;

    /**
     * Socket to connect to Redis server.
     *
     * @var Redis
     */
    protected $_sock = null;

    /**
     * Storage config.
     *
     * @var array
     */
    private $_config;

    /**
     * Constructs an object of <code>SpicaSessionRedisStore</code>.
     *
     * @param array $config [host, port, db] Defaults to an empty array
     */
    public function __construct($config = array())
    {
        $default = array(
            'host' => '127.0.0.1',
            'port' => 6379,
            'persistent' => false,
            'weight' => 1,
            'timeout' => 1, // connection timeout
            'retry_interval' => 3
        );

        $this->_config = array_merge($default, $config);
    }

    /**
     * Opens session
     *
     * @param string $savePath ignored
     * @param string $sessName ignored
     * @return bool
     */
    public function open($savePath, $sessName)
    {
        $this->_lifeTime = ini_get('session.gc_maxlifetime');

        $sock = fsockopen($this->_config['host'], $this->_config['port'], $errno, $errmsg, $this->_config['timeout']);

        if (false === $sock)
        {
            $msg = "Cannot open socket to session store {$this->_config['host']}:{$this->_config['port']}";
            $msg .= ',' . ($errno ? " error $errno" : "") . ($errmsg ? " $errmsg" : "");
            trigger_error($msg, E_USER_NOTICE);
            return false;
        }

        $this->_sock = $sock;
        return true;
    }

    /**
     * Fetches session data
     *
     * @param  string $sid
     * @return string
     */
    public function read($sid)
    {
        if (null === $this->_sock)
        {
            return false;
        }

        if (!empty($this->_config['db']))
        {
            $this->_sendIntoSocket("SELECT {$this->_config['db']}\r\n");
            $this->_readRawReply();
        }

        $this->_sendIntoSocket("GET $sid\r\n");
        $value = $this->_readRawReply();

        if ('bulk' === $value[0])
        {
            $data = unserialize($value[1]);

            if (($data['modified'] + $this->_lifeTime) > time())
            {
                return $data['data'];
            }

            $this->destroy($sid);
        }

        return '';
    }

    /**
     * Closes session
     *
     * @return bool
     */
    public function close()
    {
        if (null !== $this->_sock)
        {
            fclose($this->_sock);
        }

        $this->_sock = null;
    }

    /**
     * Updates session.
     *
     * @param  string $sid Session ID
     * @param  string $data
     * @return bool
     */
    public function write($sid, $data)
    {
        if (null === $this->_sock)
        {
            return false;
        }

        $data = serialize(array('modified' => time(), 'data' => $data));
        $this->_sendIntoSocket("GET $sid\r\n");
        $value = $this->_readRawReply();

        if ('error' === $value[0])
        {
            return false;
        }

        if ('bulk' === $value[0] && '-1' !== $value[1])
        {
            $this->_increment(); // New session. Possibly
        }

        $this->_sendIntoSocket("SET $sid " . strlen($data) . "\r\n$data\r\n");
        $value = $this->_readRawReply();

        return true;
    }

    /**
     * Destroys session provided with ID.
     *
     * @param  string $sid
     * @return bool
     */
    public function destroy($sid)
    {
        if (null === $this->_sock)
        {
            return false;
        }

        $this->_sendIntoSocket("DEL $sid\r\n");
        $value = $this->_readRawReply();

        if ('error' === $value[0])
        {
            return false;
        }

        if ('integer' === $value[0])
        {
            if ($value[1] > 0)
            {
                $this->_decrement();
            }

            return true;
        }

        return true;
    }

    /**
     * Garbage collection
     *
     * @param  int $sessMaxLifeTime ignored
     * @return bool
     */
    public function gc($sessMaxLifeTime)
    {
        return true;
    }

    /**
     * Gets total session.
     *
     * @return int
     */
    public function getSessionCount()
    {
        if (null === $this->_sock)
        {
            return false;
        }

        $this->_sendIntoSocket("GET spica_total_session_count\r\n");
        $value = $this->_readRawReply();

        if ('bulk' === $value[0])
        {
            if ('-1' === $value[1])
            {
                return 0;
            }

            return $value[1];
        }

        return 0;
    }

    /**
     * Increments total session count.
     *
     * @return bool
     */
    protected function _increment()
    {
        if (null === $this->_sock)
        {
            return false;
        }

        $this->_sendIntoSocket("INCR spica_total_session_count\r\n");
        return $this->_readRawReply();
    }

    /**
     * Decreases total session count.
     *
     * @return bool
     */
    protected function _decrement()
    {
        if (null === $this->_sock)
        {
            return false;
        }

        $this->_sendIntoSocket("DECR spica_total_session_count\r\n");
        return $this->_readRawReply();
    }

    /**
     * Sends a string throught socket to remote host.
     *
     * @param string $s
     */
    private function _sendIntoSocket($s)
    {
        while (!empty($s))
        {
            // number of bytes written
            $i = fwrite($this->_sock, $s);

            if ($i == 0)
            {
                break;
            }

            $s = substr($s, $i);
        }
    }

    /**
     * Gets Redis response.
     *
     * @return array [0 => 'status|integer|bulk|multibulk|error', 1 => array()]
     */
    private function _readRawReply()
    {
        switch (fgetc($this->_sock))
        {
            case '+': // status code reply
                return array('status', $this->_readSingleLineReply('+'));

            case ':': // integer reply
                return array('integer', $this->_readSingleLineReply(':'));

            case '$': // Bulk reply
                return array('bulk', $this->_readBulkReply());

            case '*': // Multibulk reply
                return array('multibulk', $this->_readMultibulkReply());

            case '-': // error reply
                return array('error', $this->_readSingleLineReply('-'));
        }
    }

    /**
     * Gets Redis's single line response.
     *
     * @param string $c A character
     */
    private function _readSingleLineReply($c)
    {
        return rtrim(fgets($this->_sock), "\r\n");
    }

    /**
     * Gets the bulk response (and discards the last CRLF).
     *
     * @return null|string
     */
    private function _readBulkReply()
    {
        $len = (int) fgets($this->_sock);

        if ($len == -1)
        {
            return null;
        }

        $rs  = '';

        while (strlen($rs) < $len)
        {
            $rs .= fread($this->_sock, $len);
        }

        fread($this->_sock, 2);
        return $rs;
    }

    /**
     * Gets Redis's multi bulk response.
     *
     * @return array
     */
    private function _readMultibulkReply()
    {
        $rows = (int) fgets($this->_sock);

        if ($rows == -1)
        {
            return null;
        }

        $rs = array();

        for ($i = 0; $i < $rows; $i++)
        {
            switch (fgetc($this->_sock))
            {
                case '$':
                    $rs[] = $this->_readBulkReply();
                    break;

                case ':':
                    $rs[] = (int) $this->_readSingleLineReply(':');
                    break;

                case '+':
                    $rs[] = $this->_readSingleLineReply('+');
                    break;
            }
        }

        return $rs;
    }
}

?>